{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<center>\n",
    "<img src=\"../../img/ods_stickers.jpg\">\n",
    "## Открытый курс по машинному обучению\n",
    "<center>Автор материала: Егор Лабинцев – @egor_labintcev"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"https://habrastorage.org/web/04d/883/420/04d8834204974f0baf14dc277b634e16.jpg\"/>\n",
    "\n",
    "                         **Случайная картинка из выдачи google по запросу \"небаланс классов\" :**"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Краткая постановка проблемы\n",
    "\n",
    "В задачах классификации баланс классов часто нарушается, и обычно именно меньший класс является целевым.\n",
    "\n",
    "Фрод, отказы техники, положительные медицинские диагнозы, нежелательная выдача в поисковике, отток -- лишь часть примеров таких задач. \n",
    "\n",
    "Почему обычные алгоритмы (без шаманства) не слишком хорошо работают?\n",
    "\n",
    "Если в общих чертах, то дело в том, что внутри многих алгоритмов зашита какая-либо оптимизация [loss](https://en.wikipedia.org/wiki/Loss_function)-функции, часто не учитывающей баланс классов в выборке. Именно поэтому модель стремится предсказать как можно **больше** объектов бОльшего класса, игнорируя меньший класс, но снижая общий error-rate.\n",
    "\n",
    "В этом tutorial мы рассмотрим некоторые методы, которые позволяют бороться с проблемой неравных классов. \n",
    "План такой:\n",
    "\n",
    "* Внутренние ручки алгоритмов (+ алгоритм для несбалансированных классов)\n",
    "* Библиотека imbalanced-learn"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "В качестве модельного датасета будут выступать данные о раздачах в покере (hand), где признаками будут являться карты (масть -- Suit, ранг -- C), а target -- Poker Hand, т.е. различные комбинации имеющихся карт. Датасет можно скачать [здесь](https://archive.ics.uci.edu/ml/datasets/Poker+Hand)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Выдержка из описания features датасета:\n",
    "\n",
    "```\n",
    "1) S1 \"Suit of card #1\" \n",
    "Ordinal (1-4) representing {Hearts, Spades, Diamonds, Clubs} \n",
    "\n",
    "2) C1 \"Rank of card #1\" \n",
    "Numerical (1-13) representing (Ace, 2, 3, ... , Queen, King\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Выдержка о target из описания [датасета](https://archive.ics.uci.edu/ml/datasets/Poker+Hand):\n",
    "\n",
    "```\n",
    "0: Nothing in hand; not a recognized poker hand\n",
    "1: One pair; one pair of equal ranks within five cards\n",
    "2: Two pairs; two pairs of equal ranks within five cards\n",
    "3: Three of a kind; three equal ranks within five cards\n",
    "4: Straight; five cards, sequentially ranked with no gaps\n",
    "5: Flush; five cards with the same suit\n",
    "6: Full house; pair + different rank three of a kind\n",
    "7: Four of a kind; four equal ranks within five cards\n",
    "8: Straight flush; straight + flush\n",
    "9: Royal flush; {Ace, King, Queen, Jack, Ten} + flush \n",
    "```\n",
    "\n",
    "Я не большой спец в покере, но чем больше цифра, тем реже класс и тем выигрышнее позиция."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Для примеров нам понадобится .py [скрипт](https://github.com/silicon-valley-data-science/learning-from-imbalanced-classes/blob/master/blagging.py) blagging.py, который можно просто положить рядом с ноутбуком, а также библиотека [imbalanced-learn](http://contrib.scikit-learn.org/imbalanced-learn/index.html):\n",
    "\n",
    "`pip install -U imbalanced-learn`\n",
    "\n",
    "или для Anaconda \n",
    "\n",
    "`conda install -c glemaitre imbalanced-learn`"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Загрузка библиотек\n",
    "\n",
    "import matplotlib.pyplot as plt\n",
    "import numpy as np\n",
    "import pandas as pd\n",
    "from sklearn import neighbors\n",
    "from sklearn.ensemble import (ExtraTreesClassifier, GradientBoostingClassifier,\n",
    "                              RandomForestClassifier)\n",
    "from sklearn.linear_model import LogisticRegression\n",
    "from sklearn.metrics import (auc, precision_recall_curve, roc_auc_score,\n",
    "                             roc_curve)\n",
    "from sklearn.model_selection import StratifiedKFold\n",
    "from sklearn.preprocessing import OneHotEncoder\n",
    "\n",
    "%matplotlib inline\n",
    "plt.rcParams['figure.figsize'] = (10, 8)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Внутренние ручки алгоритмов"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Загрузим данные и посмотрим на них:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def load_and_prepare_data():\n",
    "    \n",
    "    # Загрузим данные\n",
    "\n",
    "    df = pd.read_csv('poker-hand-training-true.data', \n",
    "                     names=['Suit1', 'C1', 'Suit2', 'C2', 'Suit3', \n",
    "                            'C3', 'Suit4', 'C4', 'Suit5', 'C5', 'CLASS'])\n",
    "    \n",
    "    # кодирование порядковых (ordinal) признаков -- отдельная тема, здесь обойдемся one-hot\n",
    "\n",
    "    ordinal_columns = [col for col in df.columns if 'Suit' in col]\n",
    "\n",
    "    ohe = OneHotEncoder(sparse=False)\n",
    "    encoded_ordinal = ohe.fit_transform(df[ordinal_columns])\n",
    "\n",
    "    # удаляем оригинальные колонки\n",
    "    df.drop(ordinal_columns, axis=1, inplace=True)\n",
    "    \n",
    "    tmp = pd.DataFrame(encoded_ordinal, columns=['S ' + str(i) for i in range(encoded_ordinal.shape[1])])\n",
    "    df = pd.concat([df, tmp], axis=1)\n",
    "    \n",
    "    return df"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df = load_and_prepare_data()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.head(10)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Взглянем на распределение классов в выборке, чтобы оценить несбалансированность данных."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Распределение классов в выборке\n",
    "\n",
    "print(\"Initial class percentages: \\n\")\n",
    "df.CLASS.value_counts()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X = df.drop('CLASS', axis=1).as_matrix()\n",
    "y = df.CLASS"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Посмотрим на качество алгоритмов as is, предварительно разбив данный на train и test.\n",
    "\n",
    "В процессе просмотра метрик рекомендую особенно обращать внимание на `recall` классов или на F1-меру."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.model_selection import train_test_split\n",
    "\n",
    "RANDOM_STATE = 42\n",
    "X_train, X_test, y_train, y_test = train_test_split(X, y, stratify=y, \n",
    "                                                    test_size=0.4, random_state=RANDOM_STATE)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.metrics import classification_report"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "clf = RandomForestClassifier(random_state=RANDOM_STATE)\n",
    "\n",
    "clf.fit(X_train, y_train)\n",
    "\n",
    "print(classification_report(y_test, clf.predict(X_test)))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Как мы видим, наиболее редкие классы дефолтный RandomForest не нашел и ругается на отсутствие предиктов по ним.\n",
    "\n",
    "Представим на секунду, что нас интересуют классы с 6 по 9 включительно, т.е. редкие, но резко повышающие вероятность победить. Посмотрим, возможно ли подобрать веса таким образом, чтобы найти эти классы."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "В некоторых алгоритмах существует возможность проставить `class_weight`, например, у всеми любимой логистической регрессии и случайного леса и таким образом скорректировать штраф за неверно предсказанный объект. Альтернативой ручном подбору является `'balanced'` опция, проставляющая веса в соотвествии с распределением в обучающей выборке."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "searching_for_classes = ['balanced', \n",
    "                         {6:2, 7:2, 8:2, 9:2},\n",
    "                         {6:10, 7:10, 8:10, 9:10}\n",
    "                        ]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for option in searching_for_classes:\n",
    "    \n",
    "    clf = RandomForestClassifier(class_weight=option, random_state=RANDOM_STATE)\n",
    "    clf.fit(X_train, y_train)\n",
    "\n",
    "    print(classification_report(y_test, clf.predict(X_test)))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Теперь посмотрим на ExtraTreesClassifier"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for option in searching_for_classes:\n",
    "    \n",
    "    clf = ExtraTreesClassifier(class_weight=option, random_state=RANDOM_STATE)\n",
    "    clf.fit(X_train, y_train)\n",
    "\n",
    "    print('weights: ' + str(option) + '\\n' + classification_report(y_test, clf.predict(X_test)) + '\\n' )"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Видим, что в первом случае (при \"'balanced'\") мы теперь находим 5-ый класс. Впрочем, это не совсем то, чего мы хотели. Посмотрим на вероятности, проставленные классификатором для каждого из классов."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "predicted_probs = clf.predict_proba(X_test)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "pd.DataFrame(predicted_probs, \n",
    "             columns=['prob_' + str(i) for i in range(0,10)]) \\\n",
    "             [[\"prob_6\", \"prob_7\", \"prob_8\", \"prob_9\"]].describe()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Можно было бы воспользоваться стандартным приемом и выкрутить порог по вероятностям, т.е. назначать класс при меньшем, чем дефолтный 0.5 пороге, поймав часть экземпляров класса. Однако мы видим, что в данном случае такой подход не имеет смысла -- вероятности крайне малы и таких примеров совсем немного."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Мы еще вернемся к проблеме определения столь малых классов."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Blagging Classifier"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Теперь посмотрим на работу Blagging Classifier'а, который из коробки умеет балансировать классы.\n",
    "\n",
    "Отличное интуитивное представление о работе этого классификатора даст [этот](https://github.com/silicon-valley-data-science/learning-from-imbalanced-classes/blob/master/Gaussians.ipynb) ноутбук, а саму статью с подходом можно найти [здесь](https://pdfs.semanticscholar.org/a8ef/5a810099178b70d1490a4e6fc4426b642cde.pdf).\n",
    "\n",
    "Ну и исходный [код](https://github.com/silicon-valley-data-science/learning-from-imbalanced-classes/blob/master/blagging.py), конечно.\n",
    "\n",
    "\n",
    "В общий чертах подход следующий:\n",
    "\n",
    "* Bootstrap из датасета\n",
    "* Балансирование путем уменьшения размера большего класса (downsampling)\n",
    "* Обучение Decision Tree на каждой из выборок\n",
    "* Majority vote по набору деревьев"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"https://habrastorage.org/web/29a/31c/af6/29a31caf67f8449dace109394b8b7e6a.png\"/>\n",
    "\n",
    "Картинка [отсюда](https://svds.com/learning-imbalanced-classes/)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Жаль, но этот классификатор работает только для бинарной классификации, так что мы сведем задачу к такому виду. Пусть у нас есть редкие, но выигрышные классы и несколько классов с наибольшим количеством примеров, не являющиеся выигрышными."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df = load_and_prepare_data()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# еще раз посмотрим на распределение классов\n",
    "\n",
    "df.CLASS.value_counts()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def make_binary(original_data, pos_classes):\n",
    "    return np.array([(1 if val in pos_classes else 0)\n",
    "                     for val in original_data ])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "binary_y = make_binary(df.CLASS, set((4, 5, 6, 7, 8, 9)))\n",
    "print(\"After merging classes {0, 1, 2, 3} -> 0 and {4, 5, 6, 7, 8, 9} -> 1 \\n\")\n",
    "\n",
    "np.unique(binary_y, return_counts=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Проблема осталось крайне несбалансированной, давайте проверим как поведут себя алгоритмы sklearn и сравним с BlaggingClassifier'ом."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X = df.drop('CLASS', axis=1).as_matrix()\n",
    "y = binary_y\n",
    "\n",
    "X_train, X_test, y_train, y_test = train_test_split(X, y, stratify=y, \n",
    "                                                    test_size=0.4, random_state=RANDOM_STATE)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "RandomForest:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "clf = RandomForestClassifier(random_state=RANDOM_STATE)\n",
    "\n",
    "clf.fit(X_train, y_train)\n",
    "\n",
    "print(classification_report(y_test, clf.predict(X_test)))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "GradientBoosting:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "clf = GradientBoostingClassifier(random_state=RANDOM_STATE)\n",
    "\n",
    "clf.fit(X_train, y_train)\n",
    "\n",
    "print(classification_report(y_test, clf.predict(X_test)))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "По-прежнему никаких значительных улучшений, даже после merge классов."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from blagging import BlaggingClassifier\n",
    "\n",
    "clf = BlaggingClassifier(random_state=RANDOM_STATE)\n",
    "\n",
    "clf.fit(X_train, y_train)\n",
    "\n",
    "print(classification_report(y_test, clf.predict(X_test)))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Как мы видим, `BlaggingClassifier` отлично показал себя, выдав приличную полноту для такой задачи.\n",
    "\n",
    "Для меня был сюрпризом результат ExtraTreesClassifier:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.ensemble import ExtraTreesClassifier\n",
    "\n",
    "clf = ExtraTreesClassifier(random_state=RANDOM_STATE)\n",
    "\n",
    "clf.fit(X_train, y_train)\n",
    "\n",
    "print(classification_report(y_test, clf.predict(X_test)))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "Можно подумать (но мы не будем) о том, как объединить предсказания ExtraTreesClassifier и BlaggingClassifier для лучшего результата."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Давайте теперь посмотрим на AUC-ROC для каждого из классификаторов.\n",
    "Надо заметить, что стоит строить AUC-ROC на кросс-валидации по фолдам, т.к. некоторые из объектов могут быть нетипичными для класса и это будет заметно при разбиении, а также оценка будет менее смещенной, но мы ограничимся разделением на train-test для демонстрации методов."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "clfs = [\n",
    "        ['RandomForestClassifier', RandomForestClassifier(random_state=RANDOM_STATE)],\n",
    "        ['GradientBoostingClassifier', GradientBoostingClassifier(random_state=RANDOM_STATE)],\n",
    "        ['ExtraTreesClassifier', ExtraTreesClassifier(random_state=RANDOM_STATE)], \n",
    "        ['BlaggingClassifier', BlaggingClassifier(random_state=RANDOM_STATE)]\n",
    "       ]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "fig = plt.figure()\n",
    "ax = fig.add_subplot(1, 1, 1)\n",
    "\n",
    "for name, clf in clfs:\n",
    "    \n",
    "    clf.fit(X_train, y_train)\n",
    "    fpr, tpr, thresholds = roc_curve(y_test, clf.predict_proba(X_test)[:, 1])\n",
    "    roc_auc = roc_auc_score(y_test, clf.predict_proba(X_test)[:, 1])\n",
    "    plt.plot(fpr, tpr, linestyle='-',\n",
    "             label='{} (area = %0.2f)'.format(name) % roc_auc)\n",
    "    \n",
    "plt.plot([0, 1], [0, 1], linestyle='--', color='k',\n",
    "         label='Random')    \n",
    "ax.spines['top'].set_visible(False)\n",
    "ax.spines['right'].set_visible(False)\n",
    "ax.get_xaxis().tick_bottom()\n",
    "ax.get_yaxis().tick_left()\n",
    "ax.spines['left'].set_position(('outward', 10))\n",
    "ax.spines['bottom'].set_position(('outward', 10))\n",
    "plt.xlim([0, 1])\n",
    "plt.ylim([0, 1])\n",
    "plt.xlabel('False Positive Rate')\n",
    "plt.ylabel('True Positive Rate')\n",
    "plt.title('Receiver operating characteristic')\n",
    "\n",
    "plt.legend(loc=\"lower right\")\n",
    "\n",
    "plt.show()    "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Видно, что за счет \"сваливания предсказаний классификатора в больший класс, градиентный бустинг выигрывает у BlaggingClassifier'а.\n",
    "\n",
    "Но давайте посмотрим на AUC-PR и полноту по редкому классу и всё встанет на свои места."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.metrics import recall_score\n",
    "\n",
    "fig = plt.figure()\n",
    "ax = fig.add_subplot(1, 1, 1)\n",
    "\n",
    "for name, clf in clfs:\n",
    "    \n",
    "    clf.fit(X_train, y_train)\n",
    "    fpr, tpr, thresholds = precision_recall_curve(y_test, clf.predict_proba(X_test)[:, 1])\n",
    "    recall_1 = recall_score(y_test, clf.predict(X_test))\n",
    "    plt.plot(fpr, tpr, linestyle='-',\n",
    "             label='{} (recall_1 = %0.2f)'.format(name) % recall_1)\n",
    "    \n",
    "    \n",
    "ax.spines['top'].set_visible(False)\n",
    "ax.spines['right'].set_visible(False)\n",
    "ax.get_xaxis().tick_bottom()\n",
    "ax.get_yaxis().tick_left()\n",
    "ax.spines['left'].set_position(('outward', 10))\n",
    "ax.spines['bottom'].set_position(('outward', 10))\n",
    "plt.xlim([0, 1])\n",
    "plt.ylim([0, 1])\n",
    "plt.xlabel('Recall')\n",
    "plt.ylabel('Precision')\n",
    "plt.title('Precision-Recall curve')\n",
    "\n",
    "plt.legend(loc=\"upper right\")\n",
    "\n",
    "plt.show()    "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Здесь мы видим, что наибольшая `recall` меньшего класса именно у Blagging Classifier'а."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Библиотека imbalanced-learn"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "[Библиотека](http://contrib.scikit-learn.org/imbalanced-learn/install.html) imbalanced-learn позволяет использовать различные техники сэмплирования (как over, так и under, а также их комбинации). Позади некоторых техник стоит нетривиальные подходы, не влезающие в данный туториал, но я оставлю ссылки.\n",
    "\n",
    "В библиотеку входят:\n",
    "\n",
    "* Under-sampling methods. Всё просто, сэмплируем из б**о**льшего класса для выравнивания выборки по меньшему классу. Возможны два варианта: \n",
    " - генерация новых примеров из большего класса на основе [центроид](http://contrib.scikit-learn.org/imbalanced-  learn/generated/imblearn.under_sampling.ClusterCentroids.html) кластеров;\n",
    " - [выбор](http://contrib.scikit-learn.org/imbalanced-learn/api.html#module-imblearn.under_sampling.prototype_selection) примеров из большего класса разными способами (их реально много)\n",
    "\n",
    "\n",
    "* Over-sampling methods. Тут тоже всё просто -- мы добавляем в датасет примеры меньшего класса, просто [копируя](http://contrib.scikit-learn.org/imbalanced-learn/generated/imblearn.over_sampling.RandomOverSampler.html) или используя более хитрые техники как, например, [SMOTE](http://contrib.scikit-learn.org/imbalanced-learn/generated/imblearn.over_sampling.SMOTE.html), который позволяет генерировать синтетические примеры на основе близости нескольких соседей в признаковом пространстве, создавая (с включением случайности) новый вектор признаков для нового примера. [Тут](https://www.cs.cmu.edu/afs/cs/project/jair/pub/volume16/chawla02a-html/node6.html#SECTION00042000000000000000) подробнее."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Давайте посмотрим как поведут себя эти методы на оригинальном датасете при наличии всех классов."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df = load_and_prepare_data()\n",
    "\n",
    "X = df.drop('CLASS', axis=1).as_matrix()\n",
    "y = df.CLASS\n",
    "\n",
    "X_train, X_test, y_train, y_test = train_test_split(X, y, stratify=y, \n",
    "                                                    test_size=0.4, random_state=RANDOM_STATE)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.CLASS.value_counts()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Начнем с простого -- обычный undersampling:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from imblearn.pipeline import make_pipeline\n",
    "from imblearn.under_sampling import RandomUnderSampler\n",
    "\n",
    "pipe = make_pipeline(RandomUnderSampler(random_state=RANDOM_STATE), \n",
    "                     ExtraTreesClassifier(random_state=RANDOM_STATE))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "pipe.fit(X_train, y_train)\n",
    "\n",
    "print(classification_report(y_test, pipe.predict(X_test)))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Уже неплохо (5,6 и 7 классы), но мы сильно просели по большим классам."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Попробуем [CondensedNearestNeighbour](http://machinelearning.org/proceedings/icml2005/papers/004_Fast_Angiulli.pdf):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from imblearn.under_sampling import CondensedNearestNeighbour\n",
    "\n",
    "pipe = make_pipeline(CondensedNearestNeighbour(random_state=RANDOM_STATE), \n",
    "                     ExtraTreesClassifier(random_state=RANDOM_STATE))\n",
    "\n",
    "pipe.fit(X_train, y_train)\n",
    "\n",
    "print(classification_report(y_test, pipe.predict(X_test)))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Уже лучше, мы снова видим большие классы!"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Надо заметить, что многие из методов, доступных в imbalanced-learn не работает для мультиклассовой постановки задачи. Поэтому вернемся к бинарной постановке для демонстрации подхода over-sampling."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df = load_and_prepare_data()\n",
    "binary_y = make_binary(df.CLASS, set((4, 5, 6, 7, 8, 9)))\n",
    "\n",
    "print(\"After merging classes {0, 1, 2, 3} -> 0 and {4, 5, 6, 7, 8, 9} -> 1 \\n\")\n",
    "\n",
    "np.unique(binary_y, return_counts=True)\n",
    "X = df.drop('CLASS', axis=1).as_matrix()\n",
    "y = binary_y\n",
    "\n",
    "X_train, X_test, y_train, y_test = train_test_split(X, y, stratify=y, \n",
    "                                                    test_size=0.4, random_state=RANDOM_STATE)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from imblearn.over_sampling import RandomOverSampler\n",
    "\n",
    "pipe = make_pipeline(RandomOverSampler(random_state=RANDOM_STATE), \n",
    "                     ExtraTreesClassifier(random_state=RANDOM_STATE))\n",
    "\n",
    "pipe.fit(X_train, y_train)\n",
    "\n",
    "print(classification_report(y_test, pipe.predict(X_test)))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Даже обычный over-sampling справился неплохо."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from imblearn.over_sampling import SMOTE\n",
    "\n",
    "pipe = make_pipeline(SMOTE(random_state=RANDOM_STATE), \n",
    "                     ExtraTreesClassifier(random_state=RANDOM_STATE))\n",
    "\n",
    "pipe.fit(X_train, y_train)\n",
    "\n",
    "print(classification_report(y_test, pipe.predict(X_test)))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Надо заметить, что imblearn имеет свою функции оценки качества модели, включающую precision, recall, specificity (true negative rate), f1, геометрическое среднее recall (sensitivity) и specificity, а также index balanced [accuracy](http://repositori.uji.es/xmlui/bitstream/handle/10234/23961/33068.pdf?sequence=1).\n",
    "\n",
    "Последний рассчитывается следующим образом:\n",
    "\n",
    "$$ IBA = (1 + Dominance)· Gmean^2 ,$$\n",
    "    где\n",
    "$$ Dominance = True Positive Rate - True Negative Rate $$"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from imblearn.metrics import classification_report_imbalanced\n",
    "\n",
    "print(classification_report_imbalanced(y_test, pipe.predict(X_test)))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Напоследок построим ROC-кривые для некоторых методов."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class DummySampler(object):\n",
    "\n",
    "    def sample(self, X, y):\n",
    "        return X, y\n",
    "\n",
    "    def fit(self, X, y):\n",
    "        return self\n",
    "\n",
    "    def fit_sample(self, X, y):\n",
    "        return self.sample(X, y)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "classifier = ['ExtraTreesClassifier', ExtraTreesClassifier()]\n",
    "\n",
    "samplers = [\n",
    "    ['Standard', DummySampler()],\n",
    "    ['SMOTE', SMOTE(random_state=RANDOM_STATE)],\n",
    "    ['RandomOverSampler', RandomOverSampler(random_state=RANDOM_STATE)],\n",
    "    ['RandomUnderSampler', RandomUnderSampler(random_state=RANDOM_STATE)]\n",
    "]\n",
    "\n",
    "pipelines = [\n",
    "    ['{}-{}'.format(sampler[0], classifier[0]),\n",
    "     make_pipeline(sampler[1], classifier[1])]\n",
    "    for sampler in samplers\n",
    "]\n",
    "\n",
    "\n",
    "fig = plt.figure()\n",
    "ax = fig.add_subplot(1, 1, 1)\n",
    "\n",
    "for name, clf in pipelines:\n",
    "    \n",
    "    clf.fit(X_train, y_train)\n",
    "    fpr, tpr, thresholds = roc_curve(y_test, clf.predict_proba(X_test)[:, 1])\n",
    "    roc_auc = roc_auc_score(y_test, clf.predict_proba(X_test)[:, 1])\n",
    "    plt.plot(fpr, tpr, linestyle='-',\n",
    "             label='{} (area = %0.2f)'.format(name) % roc_auc)\n",
    "    \n",
    "plt.plot([0, 1], [0, 1], linestyle='--', color='k',\n",
    "         label='Random')    \n",
    "ax.spines['top'].set_visible(False)\n",
    "ax.spines['right'].set_visible(False)\n",
    "ax.get_xaxis().tick_bottom()\n",
    "ax.get_yaxis().tick_left()\n",
    "ax.spines['left'].set_position(('outward', 10))\n",
    "ax.spines['bottom'].set_position(('outward', 10))\n",
    "plt.xlim([0, 1])\n",
    "plt.ylim([0, 1])\n",
    "plt.xlabel('False Positive Rate')\n",
    "plt.ylabel('True Positive Rate')\n",
    "plt.title('Receiver operating characteristic')\n",
    "\n",
    "plt.legend(loc=\"lower right\")\n",
    "\n",
    "plt.show()    "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Выводы\n",
    "\n",
    "* Практика -- залог успеха. Пробуйте различные подходы и алгоритмы для решения вашей задачи, комбинируйте их\n",
    "* Перепроверяйте метрики, смотрите на задачу под правильным углом -- вы не тюните метрику, а решаете задачу"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "Ссылки:\n",
    "\n",
    "* [Документация](http://contrib.scikit-learn.org/imbalanced-learn/index.html) imbalanced-learn\n",
    "* [Пост](https://svds.com/learning-imbalanced-classes/) про работу с несбалансированными выборками, [FAQ](https://svds.com/imbalanced-classes-faq/) по ним и их [репозиторий](https://github.com/silicon-valley-data-science), откуда я взял Blagging Classifier и часть кода\n",
    "\n"
   ]
  }
 ],
 "metadata": {
  "anaconda-cloud": {},
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.1"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 1
}
